﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;

namespace com.citizensoft.MessageExchange
{
    public delegate void ServerMessageReceivedHandler(Message message);

    public class Client : IDisposable
    {
        private Context _context = null;

        public event ServerMessageReceivedHandler ServerMessageReceived = null;
        public int Port { get; private set; }
        public string IPAddress { get; private set; }
        public bool UseTLS { get; private set; }
        public string ClientCertPath { get; private set; }
        public string ClientCertPassword { get; private set; }
        public string ServerHostName { get; private set; }

        private Dictionary<long, RequestResetEvent> _responseWattings = new Dictionary<long, RequestResetEvent>();

        public Client(string ipAddress, int port, bool useTLS = false, string serverHostName = null, string clientCertPath = null, string clientCertPassword = null)
        {
            this.UseTLS = useTLS;
            this.ClientCertPath = clientCertPath;
            this.ClientCertPassword = clientCertPassword;
            this.IPAddress = ipAddress;
            this.Port = port;
            this.ServerHostName = serverHostName;
        }

        private void MessageRead(IAsyncResult obj)
        {
            Message message = null;
            try
            {
                int count = _context.Stream.EndRead(obj);
                _context.AcceptBuffer(count);
                message = _context.ExtractMessage();
                _context.Stream.BeginRead(_context.Buffer, 0, _context.Buffer.Length, new AsyncCallback(MessageRead), _context);
            }
            finally
            {
                bool raiseevent = true;
                lock (_responseWattings)
                {
                    if (_responseWattings.ContainsKey(message.Sequence))
                    {
                        RequestResetEvent response = _responseWattings[message.Sequence];
                        response.Message = message;
                        raiseevent = !_responseWattings.ContainsKey(message.Sequence) || _responseWattings[message.Sequence].RaiseReceivedEvent;
                        response.Event.Set();
                    }
                }
                if (raiseevent && message != null && ServerMessageReceived != null)
                {
                    ServerMessageReceived(message);
                }
            }
        }

        private void AuthenticateServer(IAsyncResult obj)
        {
            ((SslStream)_context.Stream).EndAuthenticateAsClient(obj);
            if (((SslStream)_context.Stream).IsAuthenticated)
            {
                _context.Stream.BeginRead(_context.Buffer, 0, _context.Buffer.Length, new AsyncCallback(MessageRead), null);
            }
            else
            {
                _context.Client.Close();
            }
        }

        public bool Connect()
        {
            TcpClient client = new TcpClient();
            client.ReceiveBufferSize = 4096;
            client.SendBufferSize = 4096;
            client.Connect(IPAddress, Port);
            if (UseTLS)
            {
                SslStream stream = new SslStream(client.GetStream(), false);
                _context = new Context(client, stream);
                if (!string.IsNullOrWhiteSpace(ClientCertPath) && File.Exists(ClientCertPath))
                {
                    if (string.IsNullOrWhiteSpace(ClientCertPassword))
                    {
                        stream.AuthenticateAsClient(ServerHostName, new X509CertificateCollection(new X509Certificate[] { new X509Certificate2(ClientCertPath) }), System.Security.Authentication.SslProtocols.Tls, false);
                    }
                    else
                    {
                        stream.AuthenticateAsClient(ServerHostName, new X509CertificateCollection(new X509Certificate[] { new X509Certificate2(ClientCertPath, ClientCertPassword) }), System.Security.Authentication.SslProtocols.Tls, false);
                    }
                }
                else
                {
                    stream.AuthenticateAsClient(ServerHostName);
                }

                if (((SslStream)_context.Stream).IsAuthenticated)
                {
                    _context.Stream.BeginRead(_context.Buffer, 0, _context.Buffer.Length, new AsyncCallback(MessageRead), null);
                }
                else
                {
                    _context.Client.Close();
                    return false;
                }

            }
            else
            {
                _context = new Context(client, client.GetStream());
                _context.Stream.BeginRead(_context.Buffer, 0, _context.Buffer.Length, new AsyncCallback(MessageRead), null);
            }
            return true;
        }

        public bool Send(Message message)
        {
            if (_context == null || !_context.Client.Connected)
            {
                if (!this.Connect())
                {
                    return false;
                }
            }
            message.WriteTo(_context.Stream);
            return true;
        }

        public Message Request(Message message, TimeSpan? timeout = null, bool raiseReceivedEvent = false)
        {
            if (_context == null || !_context.Client.Connected)
            {
                if (!this.Connect())
                {
                    return null;
                }
            }
            RequestResetEvent response = new RequestResetEvent(raiseReceivedEvent);
            lock (_responseWattings)
            {
                if (_responseWattings.ContainsKey(message.Sequence)) return null;
                _responseWattings.Add(message.Sequence, response);
            }
            message.WriteTo(_context.Stream);
            if (timeout.HasValue)
            {
                response.Event.WaitOne(timeout.Value);
            }
            else
            {
                response.Event.WaitOne();
            }
            lock (_responseWattings)
            {
                _responseWattings.Remove(message.Sequence);
            }
            return response.Message;
        }

        public void Close()
        {
            if (_context != null)
            {
                if (_context.Client != null) _context.Client.Close();
                if (_context.Stream != null) _context.Stream.Close();
            }
        }

        public void Dispose()
        {
            this.Close();
        }
    }
}